1 module template_processor;
2 import std.typecons:Flag,Yes,No;
3 import std.file;
4 import std.json;
5 import std.path;
6 import std.uni;
7 
8 
9 /** 
10 dub.template.json reference:
11 
12 The params part is checked only once. Keep it at the top of the file.
13 For not conflicting with dub's internal parameters, it uses the syntax #PARAMETER
14 ```json
15  "params": {
16 	"windows": {
17 		//Defines windows specific parameters
18 	},
19 	"linux": {
20 		//Defines linux specific parameters
21 	},
22 	"SOME_GLOBAL_VAR": "This parameter can be used anywhere here by simply using #SOME_GLOBAL_VAR"
23  }
24 ```
25 
26 A dub.template.json can have a parent dub.json(or dub.template.json), this is used for separating some
27 configurations, such as the release one, since things can get hairy quite fast if not done.
28 ```json
29 "$extends": "#HIPREME_ENGINE/dub.json"
30 ```
31 
32 ## Adding the engine optional modules 
33 This can be done by using engineModules property. They will automatically
34 use the absolute path and be added to the linkedDependencies on the current section. It is checked on
35 both root and configurations.
36 
37 Another important feature of it is that the engine distributed modules requires a special distribution of hipengine_api.
38 This distribution is hipengine_api:direct. This module optimizes the function calls to instead of using function pointers,
39 it uses extern definitions, this way, it can be built as a static library.
40 This way, it is checked inside the "release" configuration, for making every of them use the subConfiguration of
41 "direct".
42 
43 ```json
44  "engineModules": [
45 	"util",
46 	"game2d",
47 	"math"
48  ]
49 ```
50 
51 Those in linkedDependencies will automatically be added a linker flag called 
52 /WHOLEARCHIVE:depName for windows on ldc compiler. 
53 Since this is an error prone operation, it may be handled by the templater.
54 Also checked in configurations.
55 ```json
56 "linkedDependencies": {
57 	"someDubDep": {"path": "the/path/to/dep"},
58 	"arsd:anything": "11.0"
59  }
60 ```
61 
62 Those in unnamed dependencies will automatically be added to the "dependencies" section.
63 If the path does not exists, it will be ignored and simply do nothing. Also checked in configurations.
64 
65 ```json
66 "unnamedDependencies": [
67 	"some/path/to/dep"
68 ]
69 ```
70 */
71 
72 
73 enum string templateName = "dub.template.json";
74 
75 private immutable string[] systems = 
76 [
77 	"windows",
78 	"linux"
79 ];
80 
81 private enum VariableType
82 {
83 	_default,
84 	currentSystem,
85 	otherSystem
86 }
87 
88 private VariableType getType(string keyName)
89 {
90 	import std.algorithm.searching : countUntil;
91 	string currentSystem = "unknown";
92 	version(Windows)
93 		currentSystem = "windows";
94 	else version(Posix)
95 		currentSystem = "linux";
96 	
97 	if(keyName == currentSystem)
98 		return VariableType.currentSystem;
99 	else if(systems.countUntil(keyName) != -1)
100 		return VariableType.otherSystem;
101 	return VariableType._default;
102 }
103 
104 bool moduleHasDirect(string moduleName)
105 {
106 	switch(moduleName)
107 	{
108 		case "game2d":return true;
109 		default: return false;
110 	}
111 }
112 
113 
114 /** 
115  * 
116  * Params:
117  *   str = Any string
118  *   start = Where the check will start
119  *   varName = Out variable containing the variable name found
120  * Returns: The index where the search stopped
121  */
122 private long getVariableName(in string str, long start, out string varName)
123 {
124 	assert(str[start] == '#');
125 	long curr = start+1;
126 	while(curr < str.length)
127 	{
128 		char ch = str[curr];
129 		if(!(ch.isNumber || ch.isAlpha || ch == '_'))
130 			break;
131 		curr++;
132 	}
133 	varName = str[start+1..curr];
134 	return curr;
135 }
136 
137 
138 private string processString(JSONValue json, string str)
139 {
140 	import std.exception:enforce;
141 	string returnString;
142 	size_t lastStop = 0;
143 	for(size_t i = 0; i < str.length; i++)
144 	{
145 		if(str[i] == '#')
146 		{
147 			returnString~= str[lastStop..i];
148 			string varName;
149 			i = getVariableName(str, i, varName);
150 			enforce(varName in json["params"], "Variable "~varName~" not found");
151 			returnString~= json["params"][varName].str;
152 			lastStop = i;
153 			i--; //For not updating too much
154 		}
155 	}
156 	if(lastStop != str.length) returnString~= str[lastStop..$];
157 	return returnString;
158 }
159 
160 /** 
161  * 
162  * Params:
163  *   f = The file
164  *   variables = Variables to replace in the #VARIABLE text.
165  * Returns: File with replaced text.
166  */
167 private string processFile(string f, string[string] variables)
168 {
169 	string output = "";
170 	size_t lastStop = 0;
171 	for(size_t i = 0; i < f.length; i++)
172 	{
173 		if(f[i] == '#')
174 		{
175 			output~= f[lastStop..i];
176 			string varName;
177 			i = getVariableName(f, i, varName);
178 			assert(varName in variables, "Variable "~varName~" not found");
179 			output~= escapeWindowsSep(variables[varName]);
180 			lastStop = i;
181 			i--; //For not updating too much
182 		}
183 	}
184 	if(lastStop != f.length) output~= f[lastStop..$];
185 	return output;
186 }
187 
188 /** 
189  * 
190  * Params:
191  *   json = The parsed dub.template.json
192  * Returns: The variables inside "params".
193  */
194 private string[string] getParamsInTemplate(JSONValue json)
195 {
196 	string[string] variables;
197 	if(const(JSONValue)* params = "params" in json)
198 	{
199 		foreach(key, value; params.object)
200 		{
201 			switch(getType(key))
202 			{
203 				case VariableType.currentSystem:
204 				{
205 					foreach(sysKey, sysValue; value.object)
206 						variables[sysKey] = sysValue.str;
207 					break;
208 				}
209 				case VariableType._default:
210 				{
211 					if((key in variables) is null)
212 						variables[key] = value.str;
213 					break;
214 				}
215 				default:break;
216 			}
217 		}
218 	}
219 	return variables;
220 }
221 
222 private string escapeWindowsSep(string thePath)
223 {
224 	string ret;
225 	for(size_t i = 0; i < thePath.length; i++)
226 	{
227 		if(thePath[i] == '\\')
228 		{
229 			if(i+1 >= thePath.length || thePath[i+1] != '\\')
230 				ret~= "\\\\";
231 		}
232 		else 
233 			ret~= thePath[i];
234 	}
235 	return ret;
236 }
237 
238 /** 
239  * Saves the current system variables in the cache.
240  * Saves the default type in the cache too.
241  * Params:
242  *   templatePath = Where the file containing the template json is.
243  *   projectPath = The path where the project is contained. Used for the reserved #PROJECT
244  *	 enginePath = Path where the engine is located. Used for the reserved #HIPREME_ENGINE
245  *   settings = Extra settings that will be processed inside the template.
246  *   extraVariables = Optional variables which are always defined.
247  * Returns: THe resulting string
248  */
249 private string processTemplateImpl(string templatePath, string projectPath, string enginePath, const AdditionalSetting[] settings,
250 in string[string] extraVariables)
251 {
252 	string file = readText(templatePath);
253 	JSONValue json = parseJSON(file);
254 	string[string] variables = getParamsInTemplate(json);
255 	string hipremeEngine = enginePath.absolutePath.escapeWindowsSep;
256 	string project = projectPath.absolutePath.escapeWindowsSep;
257 	if(!("params" in json))
258 		json.object["params"] = emptyObject;
259 	json["params"].object["HIPREME_ENGINE"] = hipremeEngine;
260 	json["params"].object["PROJECT"] = project;
261 	foreach(k, v; extraVariables) json["params"].object[k] = v;
262 
263 
264 	foreach(op; settings)
265 	{
266 		JSONValue inherited = emptyObject;
267 		if(op.name in json)
268 		{
269 			inherited = json;
270 			op.handler(json, emptyObject);
271 		}
272 		if("configurations" in json)
273 		{
274 			foreach(cfg; json["configurations"].array)
275 			{
276 				op.handler(cfg, inherited);
277 				cfg.object.remove(op.name);
278 			}
279 		}
280 		if(op.name in json)
281 		{
282 			json.object.remove(op.name);
283 		}
284 	}
285 	variables["PROJECT"] = projectPath.absolutePath.escapeWindowsSep;
286 	variables["HIPREME_ENGINE"] = hipremeEngine;
287 	///Push the extra variables.
288 	foreach(k, v; extraVariables) 
289 		variables[k] = v;
290 	json.object.remove("params");
291 	json.object.remove("$schema");
292 	file = processFile(json.toPrettyString(JSONOptions.doNotEscapeSlashes), variables);
293 	return file;
294 }
295 
296 
297 private struct AdditionalSetting
298 {
299 	string name;
300 	JSONValue delegate(JSONValue dubFile, JSONValue inherited = emptyObject) handler;
301 	Flag!"configAvailable" config = Yes.configAvailable;
302 }
303 private enum emptyObject = JSONValue(string[string].init);
304 private enum emptyArray = JSONValue(JSONValue[].init);
305 
306 enum TemplateProcessorResult
307 {
308     notFound,
309     invalid,
310     success
311 }
312 
313 JSONValue getDubFromTemplate(string templatePath, string enginePath)
314 {
315 	string out_jsonFile;
316 	if(processTemplate(templatePath, enginePath, out_jsonFile) != TemplateProcessorResult.success)
317 		throw new JSONException("Could not succesfully process template at path "~templatePath);
318 	return parseJSON(out_jsonFile);
319 }
320 
321 /** 
322  * 
323  * Params:
324  *   templatePath = path/to/folder/with/dub.template.json
325  *   enginePath = The engine path which will be used for the configuration engineModules
326  *   templateResult = The resulting string which can be used to cache internally or even save a file.
327  *	 additionalVariables = Additional variables that may come as an always defined. Used internally
328  * Returns: The result of the operation
329  */
330 TemplateProcessorResult processTemplate(string templatePath, string enginePath, out string templateResult,
331 in string[string] additionalVariables = string[string].init)
332 {
333     string processedPath = templatePath;
334     processedPath = processedPath.absolutePath;
335     if(!exists(templatePath))
336 	{
337 		templateResult = "Path received '" ~ templatePath ~"' does not exists";
338 		return TemplateProcessorResult.notFound;
339 	}
340     templatePath = buildPath(templatePath, templateName);
341     if(!exists(templatePath))
342 	{
343 		templateResult = "File "~ templatePath~ " does not exists";
344 		return TemplateProcessorResult.notFound;
345 	}
346     AdditionalSetting[] additionals = [
347 		{"$extends", (JSONValue json, JSONValue inherited)
348 		{
349 			import std.exception:enforce;
350 			if(!("$extends" in json))
351 				return json;
352 			string parentDub = json["$extends"].str;
353 			string[] options = [
354 				parentDub,
355 				buildPath(parentDub, "dub.json"),
356 				buildPath(parentDub, "dub.template.json")
357 			];
358 			string[] excludeKeys = ["configurations", "subPackages"];
359 			JSONValue parentJson;
360 			foreach(i, opt; options)
361 			{
362 				opt = processString(json, opt);
363 				enforce(opt != templatePath, "Parent can't point to itself.");
364 				if(exists(opt))
365 				{
366 					if(i == 2)
367 						parentJson = getDubFromTemplate(opt, enginePath);
368 					else
369 						parentJson = parseJSON(cast(string)read(opt));
370 					break;
371 				}
372 			}
373 			import std.conv:to;
374 			enforce(parentJson != JSONValue.init, "Could not find json in paths "~options.to!string);
375 			foreach(key, value; parentJson.object)
376 			{
377 				import std.algorithm.searching : countUntil;
378 				if(excludeKeys.countUntil(key) == -1)
379 				{
380 					if(!(key in json)) json.object[key] = parentJson[key];
381 					else
382 					{
383 						enforce(parentJson[key].type == json[key].type);
384 						//New values that aren't array or object will be overridden
385 						switch(json[key].type)
386 						{
387 							case JSONType.array:
388 							{
389 								JSONValue[] arr = parentJson[key].array;
390 								foreach(parentValue; arr)
391 									json[key].array ~= parentValue;
392 								break;
393 							}
394 							case JSONType.object:
395 							{
396 								foreach(parentKey, parentValue; parentJson[key].object)
397 								{
398 									if(!(parentKey in json[key]))
399 										json[key].object[parentKey] = parentValue;
400 								}
401 								break;
402 							}
403 							//If both define, child json overrides it.
404 							default: continue;
405 						}
406 					}
407 				}
408 			}
409 
410 			return json;
411 		}, No.configAvailable},
412 		{"engineModules", (JSONValue json, JSONValue inherited) 
413 		{
414 			if("engineModules" in json)
415 			foreach(mod; json["engineModules"].array)
416 			{
417 				if(!("linkedDependencies" in json))
418 					json.object["linkedDependencies"] = emptyObject;
419 				json["linkedDependencies"].object[mod.str] = ["path": buildPath(enginePath, "modules", mod.str)];
420 			}
421 			if(json["name"].str == "release")
422 			{
423 				if(!("subConfigurations" in json))
424 					json["subConfigurations"] = emptyObject;
425 				
426 				static void putDirectSubconfiguration(ref JSONValue input, ref JSONValue fromCfg)
427 				{
428 					if("engineModules" in fromCfg) 
429 					foreach(mod; fromCfg["engineModules"].array) 
430 					{
431 						if(moduleHasDirect(mod.str))
432 							input["subConfigurations"][mod.str] = "direct";
433 					}
434 				}
435 				///Put direct from inherited
436 				putDirectSubconfiguration(json, inherited);
437 				putDirectSubconfiguration(json, json);
438 			}
439 			return json;
440 		}},
441 		{"linkedDependencies", (JSONValue json, JSONValue inherited)
442 		{
443 			if(!("linkedDependencies" in json))
444 				return json;
445 			foreach(key, value; json["linkedDependencies"].object)
446 			{
447 				if(!("dependencies" in json))
448 					json.object["dependencies"] = emptyObject;
449 				if(!("lflags-windows-ldc" in json))
450 					json.object["lflags-windows-ldc"] = emptyArray;
451 				json["dependencies"].object[key] = value;
452 				json["lflags-windows-ldc"].array ~= JSONValue("/WHOLEARCHIVE:"~key);
453 			}
454 			return json;
455 		}},
456 		{"unnamedDependencies", (JSONValue json, JSONValue inherited)
457 		{
458 			if(!("unnamedDependencies" in json))
459 				return json;
460 			foreach(unnamedDep; json["unnamedDependencies"].array)
461 			{
462 				import std.stdio;
463 				import std.exception:enforce;
464 				string endingPath;
465 				JSONValue* subConfiguration;
466 				if(unnamedDep.type == JSONType.object)
467 				{
468 					enforce("path" in unnamedDep, "Unnamed dependencies with type object must contain a \"path\"");
469 					endingPath = unnamedDep["path"].str;
470 					subConfiguration = ("subConfiguration" in unnamedDep);
471 					if(subConfiguration && !("subConfigurations" in json))
472 						json.object["subConfigurations"] = emptyObject;
473 				}
474 				else
475 					endingPath = unnamedDep.str;
476 
477 				endingPath = processString(json, endingPath);
478 				import std.algorithm.searching : find;
479 				
480 				string[] dubPath = find!((string f) => exists(f))(
481 				[
482 					buildPath(processedPath, endingPath, "dub.json"),
483 					buildPath(processedPath, endingPath, "dub.template.json")
484 				]);
485 
486 				if(dubPath.length)
487 				{
488 					if(!("dependencies" in json))
489 						json.object["dependencies"] = emptyObject;
490 					JSONValue dubJson = parseJSON(readText(dubPath[0]));
491 					string packageName = dubJson["name"].str;
492 					enforce(!(packageName in json["dependencies"]), "Package "~packageName~" from path "~endingPath~" is already present in the dependencies.");
493 					json["dependencies"][packageName] = ["path": endingPath];
494 					if(subConfiguration)
495 						json["subConfigurations"].object[packageName] = subConfiguration.str;
496 				}
497 				else
498 					writeln("Warning: Unnamed dependency at path ", endingPath, " not found");
499 			}
500 			return json;
501 		}}
502 	];
503 
504     templateResult = processTemplateImpl(templatePath, processedPath, enginePath, additionals, additionalVariables);
505     return TemplateProcessorResult.success;
506 }